Android热更新调研汇总

16年热更新技术大火,林林总总出现很多开源框架和方式,最近正好有时间进行调研比较。

Android运行模式

先说下Android有三种运行模式:JNI、Dalvik、ART
dalvik明显是最慢的,完全的JNI模式是最快的但是开发难度高,ART介于两者之间,并且不影响现有开发模式

1)JNI:在开发过程中使用编译器在C/C++等语言直接编译成机器码,运行的时候能够充分利用系统性能,这是最快的。iOS的Object C和Android 的NDK都是这种模式。

2)Dalvik:Android L系统之前所有Android版本的运行方式,采用的是字节码,在运行的时候解释执行变成机器能够识别的机器码。这个过程是比较缓慢的。

3)ART:Android 4.4开始推出的新的运行环境,在APP安装的时候使用dex2oat工具直接把DEX文件转换为机器码文件,运行的时候以机器码方式运行,可以充分利用系统性能;此外,改进的内存回收机制使得ART运行模式下的内存回收速度只有Dalvik运行时模式下的50%,也能够提升系统运行速度。

ART缺点:
1)APP安装过程会变慢;

2)APP占用的存储空间会变多,系统更容易出现系统空间不足问题。

贴一下classloader机制

Java中ClassLoader的基本概念

类加载器的树状结构:在JVM中,所有类加载器实例按树状结构组织,根结点为引导类加载器。除根结点外的所有类加载器都有一个非空的父类加载器,从而构成树状结构;

双亲委托(代理)模型:当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程;

代理模式是为了保证 Java 核心库的类型安全。通过代理模式,对于 Java 核心库的类的加载工作由bootClassLoader来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

类的判等:即使类完全相同(名称相同、字节码相同),不同类加载器实例加载的类对象也是不相等的;
这条规则是Java类加载机制中非常核心的规则,它保证了类加载机制实现“类隔离”、“保护JDK中的基础类”等目标。

类的垃圾回收:只有当类加载器可被作为垃圾回收的前提下,其加载的类才有可能被回收

Android的classLoader机制

Android的Dalvik/ART虚拟机如同标准JAVA的JVM虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。因此可以利用这一点,在程序运行时手动加载Class,从而达到代码中动态加载可执行文件的目的。

在Android系统启动的时候会创建一个Boot类型的ClassLoader实例,用于加载一些系统Framework层级需要的类。由于Android应用里也需要用到一些系统的类,所以APP启动的时候也会把这个Boot类型的ClassLoader传进来。

此外,APP也有自己的类,这些类保存在APK的dex文件里面,所以APP启动的时候,也会创建一个自己的ClassLoader实例,用于加载自己dex文件中的类。

一个是BootClassLoader(系统启动的时候创建的),另一个是PathClassLoader(应用启动时创建的,用于加载当前已安装app里面的类)

Android经常使用的是PathClassLoader和DexClassLoader

PathClassLoader
官方注释:一个简单的ClassLoader的实现,工作在本地文件系统中的文件和目录的列表上,但不尝试从网络加载类。 Android使用这个类为它的系统类加载器和应用类加载器。

可以看出,Android是使用这个类作为其系统类和应用类的加载器。并且对于这个类呢,只能去加载已经安装到Android系统中的apk文件。

DexClassLoader
官方注释:一个ClassLoader的实现,从.jar和.apk文件内部加载classes.dex。这可以用于执行非安装程序作为已安装应用程序的一部分的代码。

也就是说可以加载比如sd目录下的dex文件,获取到不是已安装app里面的类。

Android中使用PathClassLoader类作为Android的默认的类加载器,PathClassLoade本身继承自BaseDexClassLoader,BaseDexClassLoader重写了findClass方法,该方法是ClassLoader的核心。

classloader特性

使用ClassLoader的一个特点就是,当ClassLoader在成功加载某个类之后,会把得到类的实例缓存起来。下次再请求加载该类的时候,ClassLoader会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,如果程序不重新启动,加载过一次的类就无法重新加载。
如果使用ClassLoader来动态升级APP或者动态修复BUG,都需要重新启动APP才能生效。

阿里的dexposed和AndFix采用了jni hook方案 Android程序比起一般Java程序在使用动态加载时麻烦在哪里 使用ClassLoader动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的Java程序,在Android程序中使用动态加载主要有两个麻烦的问题: Android中许多组件类(如Activity、Service等)是需要在Manifest文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作; Res资源是Android开发中经常用到的,而Android是把这些资源用对应的R.id注册好,运行时通过这些ID从Resource实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到R.id的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源ID根本和现有的Resource实例中保存的资源ID对不上; 说到底,一个Android程序和标准的Java程序最大的区别就在于他们的上下文环境(Context)不同。

Android中context可以给程序提供组件需要用到的功能,也可以提供一些主题、Res等资源,而现在的各种Android动态加载框架中,核心要解决的东西也正是如何给外部的新类提供上下文环境的问题。

当前框架大致分为两种类型

1.基于Native hook方式

AndFix(阿里)

1
https://github.com/alibaba/AndFix
1
http://www.toutiao.com/i6363517224870740481/?tt_from=mobile_qq&utm_campaign=client_share&app=news_article&utm_source=mobile_qq&iid=6640735415&utm_medium=toutiao_ios
  1. 无需重启

  2. andfix只能替换方法而不能增减新的字段,也不能下发类

  3. 支持2.3~6.0,但是兼容性以及稳定性较差,关键是不需要重启。

  4. 启动时间几乎无增加,不增加运行期额外的磁盘消耗。

缺点:

  1. 不支持YunOS

  2. 无法添加新类和新的字段

  3. 需要使用加固前的apk制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露。

  4. 使用加固平台可能会使热补丁功能失效(演示样例时已经验证过)。

  5. 不能更换资源文件、不能添加类等。

  6. 兼容性问题

  7. 部分手机奔溃,部分手机ANR

  8. 不能改变量的值,不过方法的添加修改,删除,都可以

  9. 需要注意多进程

  10. ART下模式无法对同一个方法进行多次更新

    Dexposed(阿里)

    优点:
    缺点:不支持art(5.0+) 比较致命,如果线上release版本进行了混淆,那写patch也是一件很痛苦的事情,反射+内部类

2.基于classloader(Multidex)

这类具体就是搞Android的ClassLoader体系,android中加载类一般使用的是PathClassLoader和DexClassLoader。

手空(超级补丁技术)

未开源(DEX分包方案)

超级补丁方案通过修改系统加载程序的顺序实现热补丁,也就是老文章中提到的ClassLoader方法。这种对系统加载流程的修改,在老版本(Dalvik虚拟机)的Android手机上会造成程序运行性能下降,而为了避免在新版本(ART虚拟机)Android系统上的稳定性问题,又不得不在补丁包中加入大量的冗余信息,导致补丁包过大。

超级补丁技术基于DEX分包方案,使用了多DEX加载的原理,大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,插入到dexElements数组的最前面,让虚拟机去加载修复完后的方法。

优势:

  1. 没有合成整包(和微信Tinker比起来),产物比较小,比较灵活

  2. 可以实现类替换,兼容性高。整篇由上层java语言主导,不存在版本兼容问题,成功率最高 (某些三星手机不起作用)

不足:

  1. 不支持即时生效,必须通过重启才能生效。

  2. 为了实现修复这个过程,必须在应用中加入两个DEX!dalvikhack.dex中只有一个类,对性能影响不大,但是对于patch.dex来说,修复的类到了一定数量,就需要花不少的时间加载。对手淘这种航母级应用来说,启动耗时增加2s以上是不能够接受的事。

  3. 在ART模式下,如果类修改了结构,就会出现内存错乱的问题。为了解决这个问题,就必须把所有相关的调用类、父类子类等等全部加载到patch.dex中,导致补丁包异常的大,进一步增加应用启动加载的时候,耗时更加严重。

Nuwa(点评,类似手空原理)

1
https://github.com/jasonross/Nuwa

优点:

  1. 比较简单

缺点:

  1. 都基于ClassLoader,兼容性以及稳定性好,支持4.0~6.0,对于启动速度有影响,补丁重启后才能生效
    ART环境支持的不好。
    i50 nuwa只支持 4.0以上,并不是作者所描述的2.3~6.0,作者使用的BaseDexClassLoader这个4.0才有的API,导致4.0以下的手机用Nuwa就必定崩溃,特此提醒

RocooFix(美团,Nuwa改动, Instant Run方案,native hook)

1
https://github.com/dodola/RocooFix
1
http://mp.weixin.qq.com/s?__biz=MzIzNDA3MDgwNA==&mid=2649230203&idx=1&sn=9d9a256565ad78ff47ae6ea92b418021&chksm=f0e75d00c790d4161747788d070ff444b9d969c61f42969a31a7c75afc94fbe38fe548789236&mpshare=1&scene=23&srcid=1219SjVbeOTqz4zhflEXa7Le#rd

RocooFix支持两种模式:
1、静态修复某种情况下需要重启应用。 (推荐使用)
2、动态修复,无需重启应用即可生效。

HotFix(百度)

1
https://github.com/dodola/HotFix

没啥特点。

DroidFix

1
https://github.com/bunnyblue/DroidFix

没啥特点。

Tinker(微信,整体替换DEX的方案)

1
https://github.com/Tencent/tinker

优势:

  1. 合成整包,不用在构造函数插入代码,防止verify,verify和opt在编译期间就已经完成,不会在运行期间进行。

  2. 性能提高。兼容性和稳定性比较高。

  3. 开发者透明,不需要对包进行额外处理。

不足:

  1. 与超级补丁技术一样,不支持即时生效,必须通过重启应用的方式才能生效。

  2. 需要给应用开启新的进程才能进行合并,并且很容易因为内存消耗等原因合并失败。

  3. 合并时占用额外磁盘空间,对于多DEX的应用来说,如果修改了多个DEX文件,就需要下发多个patch.dex与对应的classes.dex进行合并操作时这种情况会更严重,因此合并过程的失败率也会更高。

派生的增量更新

1
http://mp.weixin.qq.com/s?__biz=MzIzNDA3MDgwNA==&mid=2649230218&idx=1&sn=4134a6cc999253c17bf4645c488ef94c&chksm=f0e75df1c790d4e7b367f5d7c73cbb79690818f9f196dfa46b3724c9221896a8092fafe13acf&mpshare=1&scene=23&srcid=0109wmZUCNqgnLgzqUsy7OpQ#rd

热修复的坑和解

我们知道,多DEX方案用来解决应用方法数65k的问题,现在Google也官方支持了MultiDex的实现方案。但是,这实在是应用因方法数超出而作出的不得已的下策,但是超级补丁技术和Tinker作为一种热修复的方案,平生给应用增加了多个DEX,而多DEX技术最大的问题在于性能上的坑,因此基于这种方案的补丁技术影响应用的性能是无疑的。

启动加载时间过长

我们可以看到,超级补丁技术和Tinker都选择在Application的attachBaseContext()进行补丁DEX的加载,即使这是加载dex的最佳时机,但是依然会带来很大的性能问题,首当其冲的就是启动时间太长。

对于补丁DEX来说,应用启动时虚拟机会进行dexopt操作,将patch.dex文件转换成odex文件,这个过程非常耗时。而这个过程,又要求需要在主线程中,以同步的方式执行,否则无法成功进行修复。就DEX的加载时间,大概做了以下的时间测试。

易造成应用的ANR和Crash

正是尤其多DEX加载导致了启动时间过长,很容易就会引发应用的ANR。我们知道当应用在主线程等待超过5s以后,就会直接导致长时间无响应而退出。超级补丁技术为保证ART不出现地址错乱问题,需要将所有关联的类全部加入到补丁中,而微信Tinker采取一种差量包合并加载的方式,都会使要加载的DEX体积变得很大。这也很大程度上容易导致ANR情况的出现。

除了应用ANR以外,多DEX模式也同样很容易导致Crash情况的出现。我们知道,超级补丁技术为了保证ART设备下不出现地址错乱问题,需要把修改类的所有相关类全部加入到补丁中,这里会出现一个问题,为了保证补丁包的体积最小,能否保证引入全部的关联类而不引入无关的类呢?一旦没有引入关联的类,就会出现以下的异常:

NoClassDefFoundError

Could not find class

Could not find method

总结

QQ空间超级补丁技术和微信Tinker 支持新增类和资源的替换,在一些功能化的更新上更为强大,但对应用的性能和稳定会有的一定的影响;阿里百川HotFix虽然暂时不支持新增类和资源的替换,对新功能的发布也有所限制,但是作为一项定位为线上紧急BUG的热修复的服务来说,能够真正做到BUG即时修复用户无感知,同时保证对应用性能不产生不必要的损耗,在热修复方面不失为一个好的选择。

阿里百川HotFix:启动时间几乎无增加,不增加运行期额外的磁盘消耗。

QQ空间超级补丁技术:如果应用有700个类,启动耗时增加超过2.5s,达到5.5s以上。

微信Tinker:假设应用有5个DEX文件,分别修改了这5个DEX,产生5个patch.dex文件,就要进行5次的patch合并动作,假设每个补丁1M,那么就要多占用7.5M的磁盘空间。

所以so…

基于公司要制作自己的sdk供第三方使用,选用微信的Tinker和阿里的AndFix,因为Tinker比较成熟稳定,AndFix可以即使生效,试着集成这两种具体方案,后面贴出两种集成方案代码。